package icasue.stronger.register;

import icasue.stronger.wareable.logicstronger.StrongerAdvice;
import icasue.stronger.wareable.nameconvert.DefaultBeanNameTransFor;
import icasue.stronger.factory.ProxyStronger;
import icasue.stronger.wareable.nameconvert.BeanNameTransFor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.*;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ScannedGenericBeanDefinition;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2020/6/28 下午5:06
 * @UpdateDate:
 * @Description:
 */
@Slf4j
public class StrongerProxyClassPathBeanDefinitionScannerRegister extends ClassPathBeanDefinitionScanner {

    protected BeanDefinitionRegistry registry;
    protected Class<? extends StrongerAdvice> strongerAdvice;
    protected Class<? extends Annotation> markedAnnotation;
    protected ProxyStronger<?,?> proxyStronger = new ProxyStronger<Object,Object>();
    protected List<String> markerExclude = Arrays.asList();
    protected BeanNameTransFor beanNameTransFor = new DefaultBeanNameTransFor();
    protected String strongerAdviceRef;

    private static volatile Map<String, String> beanNameDuplicateStoreMapping =
            new ConcurrentHashMap<>(64,0.75f,1);
    private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
            | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;
    private static final MethodHandles.Lookup methodLookup;
    private static volatile Map<String, BeanDefinition> beanDefinitionMap;
    private static volatile Map<String, ?> waitingCompleteBeanWrapper;

    static {
        //get lookup
        MethodHandles.Lookup lookupTemp = null;
        try {
            //java 9
            Method lookupMethod = MethodHandles.class.getDeclaredMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class);
            lookupMethod.setAccessible(true);
            MethodHandles.Lookup lookup =
                    (MethodHandles.Lookup)lookupMethod.invoke(null, MethodHandles.Lookup.class, MethodHandles.lookup());
            lookupTemp = lookup;
        }catch (Exception e){
            //java 8
            try {
                Constructor<MethodHandles.Lookup> lookupConstruct = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
                lookupConstruct.setAccessible(true);
                lookupTemp = lookupConstruct.newInstance(MethodHandles.Lookup.class, ALLOWED_MODES);
            } catch (Exception e1) {
                throw new RuntimeException(e1.getCause());
            }
        }
        methodLookup = lookupTemp;
    }

    public StrongerProxyClassPathBeanDefinitionScannerRegister(BeanDefinitionRegistry registry) {
        super(registry,false);
        this.registry = registry;
    }

    /**
     * register packages scan filters.
     */
    public void registerFilter(){
        //flag accept all metadata which has been scanned.
        boolean acceptAllTypes = true;

        //add include filter,scan classes which metadata include this MarkedAnnotation.
        if(markedAnnotation != null){
            addIncludeFilter(new AnnotationTypeFilter(markedAnnotation));
            acceptAllTypes = false;
        }

        //add exclude filter,those classes will not be scan to BeanDefinitions.
        if(markerExclude.size() > 0){
            for (String regex : markerExclude) {
                addExcludeFilter(new TypeFilter() {
                    @Override
                    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                        ClassMetadata classMetadata = metadataReader.getClassMetadata();
                        return classMetadata.getClassName().matches(regex);
                    }
                });
            }
        }

        //scan all classes below current packages.
        if(acceptAllTypes){
            addIncludeFilter(new TypeFilter() {
                @Override
                public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                    return true;
                }
            });
        }
    }

    /**
     * do scan,generic beanDefinition and put in spring bucket.
     * @param basePackages
     * @return
     */
    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        //read beanDefinitionMap from BeanFactory.
        this.readBeanDefinitionMap();
        //do scan.
        Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
        if(beanDefinitionHolders.isEmpty()) {
        }else {
            //do postProcess for these BeanDefinitions which package scan generic.
            //then do dynamic reference and generic suitable proxy.
            processBeanDefinitions(beanDefinitionHolders);
        }
        return beanDefinitionHolders;
    }


    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitionHolders){
        //redefine BeanDefinition -> GenericBeanDefinition (ScannerGenericBeanDefinition)
        GenericBeanDefinition genericBeanDefinition;
        for (BeanDefinitionHolder holder : beanDefinitionHolders) {

            genericBeanDefinition = (ScannedGenericBeanDefinition) holder.getBeanDefinition();
            //create LbsProxyStronger factoryInstance.(actually return interface/class -> Proxy )
            String beanClassName = genericBeanDefinition.getBeanClassName();
            genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(genericBeanDefinition.getBeanClassName());
            genericBeanDefinition.setBeanClass(this.proxyStronger.getClass());
            //set component's name. for multiply's implement reference.
            genericBeanDefinition.getPropertyValues().add("componentName",holder.getBeanName());
            //try reference,for these components which already exist in spring factory,through this step to keep these reProxy instance
            //has stronger advice we excepted and as same time no affect original features such as spring transaction.
            String transNameOrFactory = Optional.ofNullable(beanNameDuplicateStoreMapping.get(holder.getBeanName()))
                            .orElse(null);
            if (StringUtils.hasText(transNameOrFactory)) {
                genericBeanDefinition.getPropertyValues().add("proxyRefObject", new RuntimeBeanReference(transNameOrFactory));
            }
            //set strongerLogic
            if (!StringUtils.isEmpty(strongerAdviceRef)){
                genericBeanDefinition.getPropertyValues().add("strongerAdvice", new RuntimeBeanReference(strongerAdviceRef));
            } else if(this.strongerAdvice != null) {
                genericBeanDefinition.getPropertyValues().add("strongerAdvice", BeanUtils.instantiateClass(this.strongerAdvice));
            }

            //enable AUTO_WIRE
            genericBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        }
    }

    /**
     * This method will be invoked when doScan execute for supplier logic
     * to check those beanDefinitions which been scanner find out is suitable to register.
     * @param beanDefinition
     * @return
     */
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        AnnotationMetadata metadata = beanDefinition.getMetadata();
        boolean virtualClassCheck = metadata.isIndependent() && metadata.isConcrete() && !metadata.isAbstract() && !metadata.isAnnotation() ?
                ( Enum.class.getName().equals(metadata.getSuperClassName())
                        ? false : ( !metadata.isFinal() || metadata.getInterfaceNames().length > 0)
                ) : false;
        //check virtualClasses's definition those want to become candidateComponent.
        if(virtualClassCheck){
            //inner class will be ignore for proxy creating.
            return beanNameGetterStandard(metadata.getClassName(),true).contains("$") ? false : true;
        }else {
            //allow interface become spring'scomponent,only if current already exist BeanDefinition in beanDefinitionMap
            //which may came form spring's base scanner or inherited other framework's definition.
            if(metadata.isInterface()){
                String className = metadata.getClassName();
                String standardBeanName = this.beanNameGetterStandard(className,true);
                if(beanDefinitionMap.containsKey(standardBeanName)
                        || beanDefinitionMap.containsKey(this.beanNameGetterStandard(className,false))){
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * This method will be invoke after isCandidateComponent,
     * used for compare and check beanDefinitions
     * which we scan discovered and already exist in beanDefinitionMap through current classType.
     * @param beanName
     * @param beanDefinition
     * @return
     * @throws IllegalStateException
     */
    @Override
    protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
        BeanDefinitionRegistry registry = getRegistry();
        if (!registry.containsBeanDefinition(beanName)) {
            return true;
        }
        BeanDefinition existingDef = registry.getBeanDefinition(beanName);
        BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
        if (originatingDef != null) {
            existingDef = originatingDef;
        }
        if (isCompatible(beanDefinition, existingDef)) {
            return true;
        }
        throw new RuntimeException("Annotation-specified bean name '" + beanName +
                "' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
                "non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
    }

    /**
     * This method is key method, will been invoke on checkCandidate().
     * @param newDefinition
     * @param existingDefinition
     * @return
     */
    @Override
    protected boolean isCompatible(BeanDefinition newDefinition, BeanDefinition existingDefinition) {
        return true;
    }

    /**
     * This method is used for registerBeanDefinition actually, also as the last step on method doScan
     * We must override this method,cause of we has reDefined some keyLogic to ensure those reProxy's component
     * that we need actual came in beanFactory. so this method is the last step for register.
     * we must reDefined this logic, when beanDefinitionMap already have same type's beanDefinition.
     * below logic :
     *      1.check if exist beanDefinition in beanDefinitionMap, when not exist,execute origin logic.
     *      2.when already same beanDefinition, retain originBeanDefinition and rePut new BeanDefinition.
     *        retain origin beanDefinition is for RuntimeBeanReference,to ensure used ourSelf's proxy,
     *        but invoker has origin features still.
     * @param definitionHolder
     * @param registry
     */
    @Override
    protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {
        try {
            //retain origin BeanDefinitions and rePut reProxy BeanDefinition,
            // and do record,for RunningTimeReference
            String beanName = definitionHolder.getBeanName();
            BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
            if(beanDefinition != null){
                log.info(" -----> 存在bean 定义 ---> \n {}" , beanDefinition);
                String oldName = beanNameTransFor.transForTo().apply(beanName);
                beanDefinitionMap.put(oldName,beanDefinitionMap.remove(beanName));
                beanDefinitionMap.put(beanName,definitionHolder.getBeanDefinition());
                // retain name mapping of proxy(newName) to originInst(oldName).
                beanNameDuplicateStoreMapping.put(beanName, oldName);
                // remove waitingCompleteBeanWrapper for origin instance, instead of Proxy instance.
                waitingCompleteBeanWrapper.remove(beanName);
            }else {
                BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
            }
        }catch (Exception e){
            // -----------
            log.info("\n--------->\n Occur an unKnow Error when convert BeanDefinition from BeanDefinitionMap or register BeanDefinition which in {}." , DefaultListableBeanFactory.class);
        }
    }

    /**
     * tansFor className to standard beanName.
     * @param classNameEtc
     * @return
     */
    private String beanNameGetterStandard(String classNameEtc,boolean ifStandard){
        int aimIndex = classNameEtc.lastIndexOf(".");
        String beanName = aimIndex < 0 ? classNameEtc : classNameEtc.substring(aimIndex + 1);
        if(ifStandard) {
            String firstChar = String.valueOf(beanName.charAt(0));
            beanName = beanName.replaceFirst(firstChar, firstChar.toLowerCase());
        }
        return beanName;
    }

    /**
     * read beanDefinitionMap from BeanFactory.
     */
    public void readBeanDefinitionMap(){
        try {
            Class<DefaultListableBeanFactory> defaultListableBeanFactoryClass = DefaultListableBeanFactory.class;
            //Getting BeanDefinitionMap from spring BeanFactory.
            if (beanDefinitionMap == null) {
                Field beanDefinitionMapField = defaultListableBeanFactoryClass.getDeclaredField("beanDefinitionMap");
                beanDefinitionMapField.setAccessible(true);
                MethodHandle beanDefinitionMapGetterMethod = methodLookup.unreflectGetter(beanDefinitionMapField);
                Map<String, BeanDefinition> beanDefinitionMap =
                        (Map<String, BeanDefinition>) beanDefinitionMapGetterMethod.bindTo(registry).invoke();
                StrongerProxyClassPathBeanDefinitionScannerRegister.beanDefinitionMap = beanDefinitionMap;

                Field factoryBeanInstanceCache = AbstractAutowireCapableBeanFactory.class.getDeclaredField("factoryBeanInstanceCache");
                factoryBeanInstanceCache.setAccessible(true);
                waitingCompleteBeanWrapper = (Map<String, ?>) factoryBeanInstanceCache.get(registry);
            }
        }catch (Throwable throwable){
            // -----------
            log.info("\n--------->\n Occur an unKnow error when get beanDefinitionMap from BeanFactory." , DefaultListableBeanFactory.class);
        }
    }

    public Class<? extends StrongerAdvice> getStrongerAdvice() {
        return strongerAdvice;
    }

    public void setStrongerAdvice(Class<? extends StrongerAdvice> strongerAdvice) {
        this.strongerAdvice = strongerAdvice;
    }

    public Class<? extends Annotation> getMarkedAnnotation() {
        return markedAnnotation;
    }

    public void setMarkedAnnotation(Class<? extends Annotation> markedAnnotation) {
        this.markedAnnotation = markedAnnotation;
    }

    public List<String> getMarkerExclude() {
        return markerExclude;
    }

    public void setMarkerExclude(List<String> markerExclude) {
        this.markerExclude = markerExclude;
    }

    public BeanNameTransFor getBeanNameTransFor() {
        return beanNameTransFor;
    }

    public void setBeanNameTransFor(BeanNameTransFor beanNameTransFor) {
        this.beanNameTransFor = beanNameTransFor;
    }

    public void setStrongerAdviceRef(String strongerAdviceRef) {
        this.strongerAdviceRef = strongerAdviceRef;
    }
}
